<!DOCTYPE html>
<html>

  <head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <title>
    编写一个最简单的-wsgi-server
    </title>
  <link type="application/atom+xml" rel="alternate" href="/feed.xml" title="虚实" />
  
  <style>
  table{
    border-left:1px solid #000000;border-top:1px solid #000000;
    width: 100%;
    word-wrap:break-word; word-break:break-all;
  }
  table th{
  text-align:center;
  }
  table th,td{
    border-right:1px solid #000000;border-bottom:1px solid #000000;
  }
  </style>

  <meta name="description" content="之前没事看了一点 flask 和 werkzeug 的源码， 就想着试一下做一个简单的 WSGI Server。">
  <link rel="stylesheet" href="/css/main.css">
  <link rel="stylesheet" href="/css/toc.css">
  <link rel="canonical" href="/2016/04/24/%E7%BC%96%E5%86%99%E4%B8%80%E4%B8%AA%E6%9C%80%E7%AE%80%E5%8D%95%E7%9A%84-wsgi-server/">
  <link rel="alternate" type="application/rss+xml" title="虚实" href="/feed.xml" />
  <link rel="stylesheet" href="/css/highlight.min.css">
  <link href="//cdn.staticfile.org/font-awesome/3.2.1/css/font-awesome.min.css" rel="stylesheet"media="all">
</head>


  <body>

    <header class="site-header">

  <div class="wrapper">
    <div>
      <a class="site-title" href="/">虚实</a>
      <div class="site-pages">
		<a class="site-page" href="/archive/">归档</a>
		<a class="site-page" href="/categories/">分类</a>
		<a class="site-page" href="/about/">关于</a>
		<a class="site-page" href="/friend-links/">友情链接</a>
        <a class="site-page" href="/recommends/">推荐</a>
      </div>
      <p class="site-sub-title">记录下折腾和学习的过程</p>
    </div>

  </div>

</header>


    <div class="page-content">
      <div class="wrapper">
        <div class="post">

  <header class="post-header">
    <h1 class="post-title">编写一个最简单的-wsgi-server</h1>
    <p class="post-meta">Apr 24, 2016 • admin</p>

	<p class="post-meta"> categories :   <a href="/categories/#网络"> 网络 </a>  </p>
    <div id="show_qrcode">
        <a>扫描二维码</a>
        <div id="qrcode" style="display:none;position:absolute;z-index:1"></div>
    </div>
  </header>
  <div class="nav">
      <div id="toc" class="toc"></div>
  </div>
  <article class="post-content">
    <p>之前没事看了一点 flask 和 werkzeug 的源码， 就想着试一下做一个简单的 WSGI Server。</p>

<p>说到 WSGI， 可以先从 CGI 说起，所谓 CGI(Common Gateway Interface)，可以理解为 Web Server 调用本地的可执行文件来参生动态内容的方式。
参考 <a href="https://en.wikipedia.org/wiki/Common_Gateway_Interface">CGI-维基百科</a>。</p>

<p>比如一个 <code class="language-plaintext highlighter-rouge">GET</code> 请求，URI 为 <code class="language-plaintext highlighter-rouge">/cgi-bin/adder?a=1&amp;b=2</code>。
此时，服务器匹配到特殊的路径 <code class="language-plaintext highlighter-rouge">/cgi-bin/</code> 于是判定这是一个 cgi 请求，请求的文件为 <code class="language-plaintext highlighter-rouge">adder</code>。
这时服务器将去寻找指定路径下名为 <code class="language-plaintext highlighter-rouge">adder</code> 的文件，检查是否存在是否有权限执行等。检查成功后将通过特殊的方式执行这个文件。</p>

<p>在这个列子中， 服务器需要将请求的 Method， 这里为GET， 还有请求的参数，即 <code class="language-plaintext highlighter-rouge">a:1, b:2</code> 传递给 adder。并且需要得到 adder 执行后的结果。</p>

<p>一般的做法是，将 method、 http header 等信息通过环境变量的方式传递给 CGI 程序。
如果是 POST 等带 request body 的方法，则将 request body 传递给 stdin。
然后 CGI 程序将 结果写回 stdout， 服务器从 stdout 读到结果后返回给客户端。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                  +-------------+
                  |             |
                  |             |
                  |  Web Server |
+------------+    |             |
|method      |    +-+---+-----^-&gt;
|http headers|      |   |     |
|etc..       +------+   |Fork |result via stdout
+via ENV     |      |   |&amp;    |
+------------+      |   |EXEC |
                  +-v---v-----+---+
                  |               |
                  |               |
                  |   CGI Program |
                  |               |
                  |               |
                  +---------------+
</code></pre></div></div>

<p>示例程序(修改自 csapp 11-34)</p>

<pre><code class="language-C">void exec_cgi(int client_fd, char *filename, char*cgiargs) {
    char *emptylist[] = {NULL};
    write(client_fd, "HTTP/1.1 200 OK\r\n");
    
    if (fork() == 0) {
        setenv("QUERY_STRING", cgiargs, 1);
        dup2(client_fd, STDOUT_FILENO);
        execve(filename, emptylist, envrion);
    }
    wait();
}
</code></pre>

<p>类似 CGI， WSGI 是起到连接 Web Server 和 Python Web 框架的作用。 不过WSGI 的目标并不只是像 CGI 一样提供动态内容，还有其他的目标，但是和这里主题不相关，详见 
<a href="https://www.python.org/dev/peps/pep-0333/">PEP 333 – Python Web Server Gateway Interface v1.0</a></p>

<p>WSGI 中，应用/框架这一端，作为被调用者，按照要求需要提供一个接受 <code class="language-plaintext highlighter-rouge">environ, start_response</code> 这两个参数的函数/类。</p>

<p>例如</p>

<pre><code class="language-Python">def app(environ, start_response):
    """Simplest possible application object"""
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return ['Hello world!\n']
</code></pre>

<p>这个函数的第一个参数为环境变量，第二个参数为一个函数，wsgi 程序需要将状态码和 附加的 header 项 传入这个函数。</p>

<p>WSGI 中，服务器这一端，作为被调用者，按特定的方式去调用 wsgi 程序</p>

<pre><code class="language-Python">import os, sys

def run_with_cgi(application):

    environ = {} # 设定的环境变量，这里略过

    # 传递给 wsgi app 的函数， 获取 status code 和 headers 后构建 response 
    # 返回给客户端， 这里省略具体内容
    def start_response(status, response_headers, exc_info=None):
        pass
        
    # 调用 wsgi 程序， 获得其返回值， 作为 response body 返回
    result = application(environ, start_response)
</code></pre>

<p><a href="https://ruslanspivak.com/lsbaws-part2/">Let’s Build A Web Server. Part 2.</a>这篇博客讲述了如何用 Python 构建一个 WSGI Server。各位可以先读一下做参考。</p>

<p>使用 Python 编写的时候调用目标时 import 后执行即可，相对容易一点。这里我们试着使用 Python/C API 来编写。因为之前的博客中提到了如何写一个简单的 HTTP Server，这里就不写如何将结果返回给客户端，只调用相应的 WSGI 程序，观察其运行结果。</p>

<p>代码部分参考 <a href="https://github.com/jonashaag/bjoern">bjoern A screamingly fast Python WSGI server written in C.</a>。</p>

<p>程序入口</p>

<pre><code class="language-C">/* 用于存放返回的结果 */
typedef struct
{
    PyObject* status; /* string */
    PyObject* headers; /* list */
    PyObject* body; /* string */
} Response;

/* 返回到 stdout 的内容在这里直接 print */
void call_app(Response* response);

int main()
{
    /* 需要 import 的 Python 文件的目录，这里设置为和编译后的可执行文件同目录 */
    setenv("PYTHONPATH",".",1); 
    /* 初始化 Python 解释器 */
    Py_Initialize();
    Response response;
    call_app(&amp;response);
    
    if (response.status != NULL) {
        printf("response-&gt;status is %s\n", PyString_AsString(response.status));
    } else {
        printf("response-&gt;status is NULL");
    }
    
    Py_Finalize();
    return 0;
}
</code></pre>

<p>初始化运行环境后直接调用了 call_app 函数，传入的是一个名为 Response 的自定义的结构。</p>

<p>call_app</p>

<pre><code class="language-C">/* 为了方便，这里直接写死了调用的 wsgi app 的文件名和函数名 */
static char *py_app = "app";
static char *py_module = "app";


void call_app(Response* response)
{
    PyObject* moduleString = PyString_FromString(py_app);
    PyObject* app_module = PyImport_Import(moduleString);
    

    /* get function reference */
    PyObject* app_func = PyObject_GetAttrString(app_module, py_app);
    printf("get app\n");
    
    /* StartResponse 为自定义的 Python type object */
    StartResponse* start_response = PyObject_NEW(StartResponse, &amp;StartResponse_Type);
    
    start_response-&gt;response = response;
    
    /* 为了方便，直接传入了固定的环境变量 */
    PyObject* env = PyDict_New();
    PyDict_SetItem(env,
                   PyString_FromString("REQUEST_METHOD"),
                   PyString_FromString("GET"));
    PyDict_SetItem(env,
                   PyString_FromString("PATH_INFO"),
                   PyString_FromString("/"));
    
    PyObject* args = PyTuple_Pack(2, env, start_response);
    printf("calling app\n");
    PyObject* output = PyObject_CallObject(app_func, args);
    
    Py_DECREF(start_response);
    
    if(!PyString_Check(output)) {
        printf("error : app should return string");
        return ;
    }
    /* 查看返回的结果 */
    printf("Application returns:%s\n", PyString_AsString(output));
}
</code></pre>

<p>可以看到，在上面的代码中 start_response 既做了被调用的函数， 又做了一个包含 response 结构的结构体。下面可以看到这是如何实现的。</p>

<pre><code class="language-C">typedef struct 
{
    PyObject_HEAD
    Response* response;
} StartResponse;

PyTypeObject StartResponse_Type = {
  PyVarObject_HEAD_INIT(NULL, 0)
  "start_response",           /* tp_name (__name__)                         */
  sizeof(StartResponse),      /* tp_basicsize                               */
  0,                          /* tp_itemsize                                */
  (destructor)PyObject_FREE,  /* tp_dealloc                                 */
  0, 0, 0, 0, 0, 0, 0, 0, 0,  /* tp_{print,getattr,setattr,compare,...}     */
  start_response              /* tp_call (__call__)                          */
};
</code></pre>

<p>可以看到 StartRespnse 是一个自定义的 Python 类， 包含 Response* 的数据。
并且在类型定义中定义了 <strong>call</strong> 的方法，所以能作为函数被调用，这里也可以给它添加一个名为 start_response 的成员函数。
关于 Python/C API 中自定义数据结构参考 <a href="https://docs.python.org/2/extending/newtypes.html">define a new type(class) in c</a></p>

<p>下面看一下这个 start_response 函数究竟如何实现的。</p>

<p>start_response</p>

<pre><code class="language-C">static PyObject*
start_response(PyObject* self, PyObject *args, PyObject *kwargs)
{
    /* 从类中获取 response 的指针  */
    Response* response = ((StartResponse*) self)-&gt;response;
    
    PyObject* status = NULL;
    PyObject* headers = NULL;
    
    if(!PyArg_UnpackTuple(args, "start_response", 2, 2, &amp;status, &amp;headers))
        return NULL;
    
    if(!PyString_Check(status)) {
        printf("start_response argument 1 should be a 'status reason' string\n");
        return NULL;
    }
    /* 将传入的 status 变量赋给 response 结构的 status 变量 */
    response-&gt;status = status;
    /* 这里暂时忽略 headers */
    if(!PyList_Check(headers)) {
        printf("start response argument 2 should be a list of 2-tuples\n");
        return NULL;
    }
    Py_INCREF(response-&gt;status);
    //Py_INCREF(self_headers);
    Py_RETURN_NONE;
}
</code></pre>

<p>简单的 wsgi 测试程序已经基本编写完成，加入一个 app.py 便可以看一下运行结果了。</p>

<p>app.py</p>

<pre><code class="language-Python">def app(env, start_response):
    status = '200 OK'
    response_headers = []
    start_response(status, response_headers)
    return "Hello World"
</code></pre>

<p>运行结果</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./wsgi                                     [23:54:43]
get app
calling app
Application returns: Hello World
response-&gt;status is 200 OK
</code></pre></div></div>

<p>完整代码在<a href="https://github.com/Mithrilwoodrat/toyws/tree/master/test">我的GitHub</a></p>

  </article>

</div>
<div id="disqus_thread"></div>
<script type="text/javascript">
    /* * * CONFIGURATION VARIABLES * * */
    var disqus_shortname = 'yoursoulismine';
    
    /* * * DON'T EDIT BELOW THIS LINE * * */
    (function() {
        var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
        dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
        (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
    })();
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript" rel="nofollow">comments powered by Disqus.</a></noscript>
<script type="text/javascript">
    /* * * CONFIGURATION VARIABLES * * */
    var disqus_shortname = 'yoursoulismine';
    
    /* * * DON'T EDIT BELOW THIS LINE * * */
    (function () {
        var s = document.createElement('script'); s.async = true;
        s.type = 'text/javascript';
        s.src = '//' + disqus_shortname + '.disqus.com/count.js';
        (document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s);
    }());
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://flowchart.js.org/flowchart-latest.js"></script>
<script src="/js/highlight.min.js"></script>
<script src="/js/toc.js"></script>
<script src="/js/qrcode.min.js"></script>
<script>
if (!String.prototype.format) {
    String.prototype.format = function() {
        var args = arguments;
        return this.replace(/{(\d+)}/g, function(match, number) { 
            return typeof args[number] != 'undefined'
                ? args[number]
                : match
            ;
        });
    };
}

$('pre code').each(function(i, block) {
    hljs.highlightBlock(block);
});

$('.language-flow').each(function(i, block) {
    console.log(block);
    code = $(this).text();
    diagram = flowchart.parse(code);
    canvas_id = 'flow'+ i;
    console.log(canvas_id);
    temp = "<div id='{0}'> </div>".format(canvas_id);
    console.log(temp);
    $(this).html(temp);
    diagram.drawSVG(canvas_id, {
        'x': 0,
        'y': 0,
        'line-width': 3,
        'line-length': 50,
        'text-margin': 10,
        'font-size': 14,
    });	
});

$('#toc').toc({
    noBackToTopLinks: true,
    listType: 'ul',
    title: 'TOC',
});

var url = location.href;
console.log(url);
var qrcode = new QRCode("qrcode", {
    text: url,
    width: 128,
    height: 128,
    colorDark : "#000000",
    colorLight : "#ffffff",
    correctLevel : QRCode.CorrectLevel.H
});

$(document).ready(function () {
    $('#show_qrcode').on('mouseenter', function () {
        $('#qrcode').show();
        $(this).css({
            "text-decoration": "underline"
        });
    }).on('mouseleave', function () {
        $('#qrcode').hide();
        $(this).css({
            "text-decoration": ''
        });
    });;
});
</script>

      </div>
    </div>

    <footer class="site-footer">

  <div class="wrapper">


    <div class="footer-col-wrapper">
      <div class="footer-col  footer-col-1">
        <ul class="contact-list">
	  <li>
          <li><a href="mailto:mithrilwoodrat@gmail.com">mithrilwoodrat@gmail.com</a></li>
        </ul>
      </div>

      <div class="footer-col  footer-col-2">
        <ul class="social-media-list">
          
          <li>
            <a href="https://github.com/Mithrilwoodrat">
              <span class="icon  icon--github">
                <svg viewBox="0 0 16 16">
                  <path fill="#828282" d="M7.999,0.431c-4.285,0-7.76,3.474-7.76,7.761 c0,3.428,2.223,6.337,5.307,7.363c0.388,0.071,0.53-0.168,0.53-0.374c0-0.184-0.007-0.672-0.01-1.32 c-2.159,0.469-2.614-1.04-2.614-1.04c-0.353-0.896-0.862-1.135-0.862-1.135c-0.705-0.481,0.053-0.472,0.053-0.472 c0.779,0.055,1.189,0.8,1.189,0.8c0.692,1.186,1.816,0.843,2.258,0.645c0.071-0.502,0.271-0.843,0.493-1.037 C4.86,11.425,3.049,10.76,3.049,7.786c0-0.847,0.302-1.54,0.799-2.082C3.768,5.507,3.501,4.718,3.924,3.65 c0,0,0.652-0.209,2.134,0.796C6.677,4.273,7.34,4.187,8,4.184c0.659,0.003,1.323,0.089,1.943,0.261 c1.482-1.004,2.132-0.796,2.132-0.796c0.423,1.068,0.157,1.857,0.077,2.054c0.497,0.542,0.798,1.235,0.798,2.082 c0,2.981-1.814,3.637-3.543,3.829c0.279,0.24,0.527,0.713,0.527,1.437c0,1.037-0.01,1.874-0.01,2.129 c0,0.208,0.14,0.449,0.534,0.373c3.081-1.028,5.302-3.935,5.302-7.362C15.76,3.906,12.285,0.431,7.999,0.431z"/>
                </svg>
              </span>
              <span class="username">Mithrilwoodrat</span> 
            </a>
          </li>
          
        </ul>
      </div>
  </div>
</footer>

<script>
  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

  ga('create', 'UA-66599686-1', 'auto');
  ga('send', 'pageview');

</script>
</body>
</html>
